from sqlalchemy.orm.interfaces import AttributeExtension
from sqlalchemy.orm.properties import ColumnProperty
from sqlalchemy.orm import column_property
from sqlalchemy.types import TypeEngine
from sqlalchemy.sql import expression
from sqlalchemy import func,literal,Column

from shapely import geometry, wkb, wkt

class GisElement(object):
  """Represents a geometry value."""
  @property
  def wkt(self):
      return func.AsText(literal(self, Geometry))
  
  @property
  def wkb(self):
      return func.AsBinary(literal(self, Geometry))
  
  def __str__(self):
      return self.desc
  
  def __repr__(self):
      return "<%s at 0x%x; %r>" % (self.__class__.__name__, id(self), self.desc)

class PersistentGisElement(GisElement):
    """Represents a Geometry value as loaded from the database."""
    
    def __init__(self, desc):
        self.desc = desc

class TextualGisElement(GisElement, expression.Function):
    """Represents a Geometry value as expressed within application code; i.e. in wkt format.
    
    Extends expression.Function so that the value is interpreted as 
    GeomFromText(value) in a SQL expression context.
    
    """
    
    def __init__(self, desc, srid=-1):
        assert isinstance(desc, basestring)
        self.desc = desc
        expression.Function.__init__(self, "GeomFromText", desc, srid)

class Geometry(TypeEngine):
  """PostGIS Geometry Type."""

  def __init__(self, SRID, type_, dimension):
    super(Geometry, self).__init__()
    self.SRID = SRID
    self.type = type_.upper()
    self.dimension = dimension

  def get_col_spec(self):
    return 'GEOMETRY'
  
  def bind_processor(self, dialect):
    """Convert from Python type to database type."""
    def process(value):
      """``value`` is a Python/Shapely geometry object."""
      if value is not None:
        return value.desc
      else:
        return value
    return process

  def result_processor(self, dialect, coltype=None):
    """Convert from database type to Python type."""
    def process(value):
      """``value`` is a hex-encoded WKB string."""
      if value is not None:
        return PersistentGisElement(value)
      else:
        return None
    return process

class POINT(Geometry):
  def __init__(self, SRID):
    super(POINT, self).__init__(SRID, 'POINT', 2)

class LINESTRING(Geometry):
  def __init__(self, SRID):
    super(LINESTRING, self).__init__(SRID, 'LINESTRING', 2)

class MULTILINESTRING(Geometry):
  def __init__(self, SRID):
    super(MULTILINESTRING, self).__init__(SRID, 'MULTILINESTRING', 2)

class MULTIPOLYGON(Geometry):
  def __init__(self, SRID):
    super(MULTIPOLYGON, self).__init__(SRID, 'MULTIPOLYGON', 2)

class GEOMETRY(Geometry):
  def __init__(self):
    super(GEOMETRY, self).__init__(0,'GEOMETRY',2)


# ORM integration
def _to_postgis(value):
  """Interpret a value as a GIS-compatible construct."""
  if hasattr(value, '__clause_element__'):
    return value.__clause_element__()
  elif isinstance(value, (expression.ClauseElement, GisElement)):
    return value
  elif isinstance(value, basestring):
    return wkt(value)
  elif value is None:
    return None
  else:
    raise Exception("Invalid type")


class GisAttribute(AttributeExtension):
  """Intercepts 'set' events on a mapped instance attribute and 
    converts the incoming value to a GIS expression.
  """
  def set(self, state, value, oldvalue, initiator):
    return _to_postgis(value)

class GisComparator(ColumnProperty.ColumnComparator):
  """Intercepts standard Column operators on mapped class attributes
  and overrides their behavior.
  """
  # override the __eq__() operator
  def __eq__(self, other):
    return self.__clause_element__().op('~=')(_to_postgis(other))
  
  # add a custom operator
  def intersects(self, other):
    return self.__clause_element__().op('&&')(_to_postgis(other))

  # any number of GIS operators can be overridden/added here
  # using the techniques above.

def GISColumn(*args, **kw):
  """Define a declarative column property with GIS behavior.
  This just produces orm.column_property() with the appropriate
  extension and comparator_factory arguments.  The given arguments
  are passed through to Column.  The declarative module extracts
  the Column for inclusion in the mapped table.
  """
  return column_property(
    Column(*args, **kw), 
    extension=GisAttribute(), 
    comparator_factory=GisComparator
  )

